De computertaal Python werd ooorspronkelijk ontworpen
als een open source computertaal die voor iedereen makkelijk
te leren en te programmeren zou zijn.
Dat verklaart de enorme populariteit ervan.
Maar door die enorme populariteit is het in de loop der tijd
toch ook een heel complexe taal geworden.
Er zijn eindeloos veel boeken verschenen over Python.
De inhoud van deze boeken wil ik niet herhalen.
Deze pagina gaat ervanuit dat je met de eerste beginselen
van de taal al kennisgemaakt hebt.
Op deze webpagina verzamel ik allerlei weetjes
die ik makkelijk wil kunnen terugvinden.
User input
Met de functie input() kun je iemand iets laten intypen. Wat de gebruiker intikt wordt altijd als 'string' geïnterpreteerd. Dat betekent dat Python de ingetypte tekst ziet als een opeenvolging van lettertekens zonder enige betekenis. Als je dus 12 intikt, dan ziet Python dat niet per se als een getal, maar als een één gevolgd door een twee. Mocht je willen dat wat ingetikt wordt anders wordt opgevat, moet je het ingetikte converteren naar het gewenste gegevenstype.
def main(): x = input('x = ') print(type(x)) if x.isnumeric(): x = int(x) print(type(x)) if __name__ == '__main__': main()
Python kent verschillende soorten gegevens. De meest bekende zijn:
letters | characters | str() |
gehele getallen | integers | int() |
drijvende-komma-getallen | floating point numbers | float() |
complexe getallen | complex numbers | |
waar of niet waar | Booleans |
In Python wordt geen decimale komma gebruikt, maar een decimale punt.
Een bestand kun je met de volgende code aanmaken:
def main(): file_object = open('outfile.csv', 'w') file_object.write("Hello\n") file_object.close() if __name__ == '__main__': main()
Voor mijn privé-programma's, vind ik csv-bestanden inlezen de gemakkelijkste vorm van invoer. Csv-bestanden kun je makkelijk aanmaken met Kladblok of Excel. Mijn voorkeur gaat uit naar de puntkomma als scheidingsteken. Het inlezen van een bestand kan in Python op verschillende manieren. De eerste manier is door een bestand te openen voor lezen, vervolgens het bestand te doorlopen met een for-loop. Bij een csv-bestand kun je in die for-loop elk record splitsen in verschillende velden. Na verwerking moet je het bestand weer te sluiten.
def main(): file_object = open('myfile.csv', 'r') for line in file_object: print(line.strip('\n')) velden = line.split(';') for v in velden: print(v) file_object.close() if __name__ == '__main__': main()
Een andere manier om een bestand in te lezen gaat met behulp van een with-context. Daarbij hoef je de file niet te sluiten, want de context van het with-statement zorgt ervoor dat dat gebeurt :
import sys def main(): filename = 'MyFile.txt' try: with open(filename) as f_input: for line in f_input: line = line.strip('\n') print(line) except Exception as err: print('(1) err') print( err ) print('(2) sys.exc_info()[0]') print( sys.exc_info()[0] ) # exception class print('(3) sys.exc_info()[1]') print( sys.exc_info()[1] ) # value print('(4) sys.exc_info()[2]') print( sys.exc_info()[2] ) # traceback object print('=====') if __name__ == '__main__': main()
Als het bestand MyFile.txt niet bestaat, geeft het programma de volgende output:
(1) err [Errno 2] No such file or directory: 'MyFile.txt' (2) sys.exc_info()[0] <class 'FileNotFoundError'> (3) sys.exc_info()[1] [Errno 2] No such file or directory: 'MyFile.txt' (4) sys.exc_info()[2] <traceback object at 0x000002C6B50EB200> =====
Je kunt het afhandelen vvan de fout in bovenstaand programma wat eleganter laten verlopen door de de class die in bovenstaande foutmelding werd genoemd, afzonderlijk af te handelen:
import sys def main(): filename = 'MyFile.txt' try: with open(filename) as f_input: for line in f_input: line = line.strip('\n') print(line) except FileNotFoundError: print('Bestand ' + filename + ' is niet aanwezig') except Exception as err: print('(1) err') print( err ) print('(2) sys.exc_info()[0]') print( sys.exc_info()[0] ) # exception class print('(3) sys.exc_info()[1]') print( sys.exc_info()[1] ) # value print('(4) sys.exc_info()[2]') print( sys.exc_info()[2] ) # traceback object print('=====') if __name__ == '__main__': main()
Als je de html-file die gaat over CSS, als invoerbestand neemt, wordt het programma maar deels uitgevoerd. Het eindigt met de volgende regels:
Als de viewport breed genoeg is, staan de verschillende blokken naast elkaar. (1) err 'charmap' codec can't decode byte 0x9d in position 3699: character maps to(2) sys.exc_info()[0] (3) sys.exc_info()[1] 'charmap' codec can't decode byte 0x9d in position 3699: character maps to (4) sys.exc_info()[2] <traceback object at 0x000001FA93C32E80> =====
Deze informatie is niet voldoende om te weten wat er aan de hand is. We breiden de code uit:
import sys, traceback def main(): filename = 'MyFile.txt' try: with open(filename) as f_input: for line in f_input: line = line.strip('\n') print(line) except FileNotFoundError: print('Bestand ' + filename + ' is niet aanwezig') except Exception as err: print('(1) err') print( err ) print('(2) sys.exc_info()[0]') print( sys.exc_info()[0] ) # exception class print('(3) sys.exc_info()[1]') print( sys.exc_info()[1] ) # value print('(4) sys.exc_info()[2]') print( sys.exc_info()[2] ) # traceback object print('(5) err.object') print( err.object ) print('(6) traceback.print_exception(err)') traceback.print_exception(err) print('(7) traceback.print_tb(err)') traceback.print_tb(err) # traceback print('=====') if __name__ == '__main__': main()
De output wordt:
Als de viewport breed genoeg is, staan de verschillende blokken naast elkaar. (1) err 'charmap' codec can't decode byte 0x9d in position 3699: character maps to <undefined> (2) sys.exc_info()[0] <class 'UnicodeDecodeError'> (3) sys.exc_info()[1] 'charmap' codec can't decode byte 0x9d in position 3699: character maps to <undefined> (4) sys.exc_info()[2] <traceback object at 0x0000022A10433940> (5) err.object Squeezed text (64 lines). (6) traceback.print_exception(err) Traceback (most recent call last): File "E:\Site\Site20240516\prive\ict\extra\test0001.py", line 7, in main for line in f_input: File "C:\Users\Wim\AppData\Local\Programs\Python\Python312\Lib\encodings\cp1252.py", line 23, in decode return codecs.charmap_decode(input,self.errors,decoding_table)[0] ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UnicodeDecodeError: 'charmap' codec can't decode byte 0x9d in position 3699: character maps to <undefined> (7) traceback.print_tb(err) Traceback (most recent call last): File "E:\Site\Site20240516\prive\ict\extra\test0001.py", line 7, in main for line in f_input: File "C:\Users\Wim\AppData\Local\Programs\Python\Python312\Lib\encodings\cp1252.py", line 23, in decode return codecs.charmap_decode(input,self.errors,decoding_table)[0] UnicodeDecodeError: 'charmap' codec can't decode byte 0x9d in position 3699: character maps to <undefined> During handling of the above exception, another exception occurred: Traceback (most recent call last): File "E:\Site\Site20240516\prive\ict\extra\test0001.py", line 29, in <module> main() File "E:\Site\Site20240516\prive\ict\extra\test0001.py", line 26, in main traceback.print_tb(err) # traceback File "C:\Users\Wim\AppData\Local\Programs\Python\Python312\Lib\traceback.py", line 55, in print_tb print_list(extract_tb(tb, limit=limit), file=file) File "C:\Users\Wim\AppData\Local\Programs\Python\Python312\Lib\traceback.py", line 74, in extract_tb return StackSummary._extract_from_extended_frame_gen( File "C:\Users\Wim\AppData\Local\Programs\Python\Python312\Lib\traceback.py", line 418, in _extract_from_extended_frame_gen for f, (lineno, end_lineno, colno, end_colno) in frame_gen: File "C:\Users\Wim\AppData\Local\Programs\Python\Python312\Lib\traceback.py", line 355, in _walk_tb_with_full_positions positions = _get_code_position(tb.tb_frame.f_code, tb.tb_lasti) AttributeError: 'UnicodeDecodeError' object has no attribute 'tb_frame'
Deze foutmelding is te complex voor mij. Ik ga op zoek op Internet, en ontdek dat anderen al meer dan 6 jaar eerder met deze foutmelding worstelden. Als mogelijke oorzaak wordt geopperd dat het bestand niet met utf-8 is aangemaakt. Ik heb de input-file aangemaakt met behulp van het Kladblok-programma in Windows. Windows schijnt niet met utf-8 te werken. De suggesties die op stackoverklow.com worden gegeven, zijn:
De eerste suggestie leidt tot:
import sys, traceback def main(): filename = 'MyFile.txt' tel = 0 try: with open(filename, 'rb') as f_input: for b_line in f_input: line = b_line.decode('utf-8') line = line.strip('\n') print(line) except FileNotFoundError: print('Bestand ' + filename + ' is niet aanwezig') except Exception as err: print('(1) err') print( err ) print('(2) sys.exc_info()[0]') print( sys.exc_info()[0] ) # exception class print('(3) sys.exc_info()[1]') print( sys.exc_info()[1] ) # value print('(4) sys.exc_info()[2]') print( sys.exc_info()[2] ) # traceback object print('(5) err.object') print( err.object ) print('(6) traceback.print_exception(err)') traceback.print_exception(err) print('(7) traceback.print_tb(err)') traceback.print_tb(err) # traceback print('=====') if __name__ == '__main__': main()
Het volgende programma telt een aantal getallen bij elkaar op.
def main(): numbers = [1, 2, 3, 5, 7, 11, 13, 17, 19, 23, 29] quantity = len(numbers) total = 0 i = 0 while i < quantity: total = total + numbers[i] i = i + 1 print(quantity) print(total) if __name__ == '__main__': main()
➋ In het while-statement wordt de voorwaarde 'i < quantity' getest. Er zijn twee plaatsen die voorafgaand aan deze test kunnen worden uitgevoerd. Beide zorgen ervoor dat de voorwaarde het juiste resultaat oplevert. Dat zijn ➊ het statement 'i = 0' voorafgaand aan het while-statement en ➌ het statement 'i = i + 1' aan het einde van block dat wordt uitgevoerd als de voorwaarde True oplevert.
Ik vind het leuk om af en toe op een primitieve manier te programmeren. Met primitief bedoel ik dat enkel gebruik wordt gemaakt van if- en while-statements. Ik gebruik dus geen for-statements, en geen object-georiënteerde technieken. Dus geen classes. Een soort heimwee naar de taal Algol, de eerste computertaal waarin ik geprogrammeerd heb.
def lees_bestand(): file_name = 'een_csv_bestand.csv' file_object = open(file_name) line = file_object.readline() print(line) while line != '': line = file_object.readline() print(line) file_object.close() lees_bestand()
Als het input-bestand de volgende inhoud heeft
David;20240112;35.00 Jim;20240123;44.00 Ken;20240125;32.81 John;20240131;4.33 David;20240204;35.00 John;20240205;21.61 Ken;20240221;12.32 David;20240228;16.37
dan ziet de output er als volgt uit:
David;20240112;35.00 Jim;20240123;44.00 Ken;20240125;32.81 John;20240131;4.33 David;20240204;35.00 John;20240205;21.61 Ken;20240221;12.32 David;20240228;16.37
Ten opzichte van de input, worden er lege regels toegevoegd.
Dat komt omdat er in het input-bestand aan het einde van elke regel
een onzichtbaar nieuwe-regel-teken staat.
Als ik het bestand inlees als binary-bestand, met het
volgende programma
input_file = open('py0051.csv', 'rb') inhoud_bestand = input_file.read() input_file.close print(inhoud_bestand)
dan krijg ik op mijn Windows-computer als output
De onzichtbare tekens \r en \n worden nu getoond. De tekenreeks begint met b gevolgd door een enkele quote (') en eindigt met een enkele quote. Dat wil zeggen dat het om een binary-weergave gaat. We gaan achterhalen welke binaire code er verscholen zit achter de tekens \r en \n.
x = b'\r' print( ord(x) ) # 13 print( hex(ord(x)) ) # 0xd x = b'\n' print( ord(x) ) # 10 print( hex(ord(x)) ) # 0xa
Op internet kun je met de zoekterm 'ascii' vinden waar \r en \n voor staan:
deci- maal | hexa- deci- maal | symbool | engels | nederlands | |
\r | 13 | D | CR | carriage return | ga naar het begin van de regel |
\n | 10 | A | LF | line feed | ga naar de volgende regel |
Hexadecimaal betekent 16-tallig. Dat betekent dat je een getal niet met de cijfers 0 t/m 9 weergeeft, maar ook A, B, C, D, E en F als cijfers beschouwt, met A=10, B=11, C=12, D=13, E=14, F =15. Dus: het decimale getal 13 komt overeen met het hexadecimale getal D, en het decimale getal 10 komt overeen met het hexadecimale getal A.
def main(): x = 'a' print( ord(x) ) # 97 print( x.encode('utf-8') ) # b'a' print( bytes(x, 'utf-8') ) # b'a' print( hex(ord(x)) ) # 0x61 print( x.encode('utf-8').hex() ) # 61 print( bytes(x, 'utf-8').hex() ) # 61 n = 97 print( chr(n) ) # a b = b'\x61' print( str(b, 'utf-8') ) # a print( b.decode('utf-8') ) # a if __name__ == '__main__': main()
In Python kun je getallen gewoon optellen. Het programma
print(2 + 3) # 5
heeft als output gewoon 5.
Maar als je in plaats van getallen letters intikt, bijvoorbeeld
print(abc + def)
dan interpreteert Python abc en def als namen van vaiabelen of als verboden tekencombinaties. Als die variabelen in het voorgaande niet dedefinieerd zijn, geeft Python een foutmelding. In dit geval wordt een syntax-error gemeld, omdat def een tekencombinatie is die je niet mag gebruiken, omdat def al gebruikt wordt om functies en methoden te definiëren.
Wel kun je tekenreeksen abc en def 'optellen' als je aanhalingstekens om abc en def zet.
print('abc' + 'def') # 'abcdef'
Het resultaat van dat 'optellen' is dat Python de tekenreeksen achter elkaar zet. Dat betekent dat het optellen van gehele getallen iets anders is dan optellen van tekenreeksen.
Het programma
print( type(2) ) # <class 'int'> print( type(3) ) # <class 'int'> print( type(5) ) # <class 'int'> print( type('abc') ) # <class 'str'> print( type('def') ) # <class 'str'>
geeft aan dat 2, 3 en 5 van het type int zijn. int is een afkorting voor integer, dat is een geheel getal; str is de afkorting voor string, wat staat voor tekenreeks. Vandaar dat 2 + 3 een andere uitkomst geeft dan '2' + '3':
print(2 + 3) # 5 print('2' + '3') # '23'
Ook andere bewerkingen als aftrekken, delen en vermenigvuldigen, werken bij verschillende gegevenstypen net iets anders.
Als je gaat delen, bijvoorbeeld
print( 5 / 2 ) # 2.5 print( 6 / 2 ) # 3.0
dan zie je dat de uitkomst een getal is waarin een decimale punt voorkomt, ook als de uitkomst van de deling een geheel getal is. Als je het type van zo'n getal met decimale komma opvraagt,
print( type(2.5) ) # <class 'float'> print( type(3.0) ) # <class 'float'>
dan blijken 2.5 en 3.0 het type float te hebben, wat wil zeggen dat het drijvende-komma-getallen zijn. Drijvende-komma-getallen worden op een speciale manier opgeteld. Dat het een andere manier van optellen is als bij gewone gehele getallen, kun je zien in het volgende programma:
print( 1 + 1 + 1 - 3) / 10 ) # 0.0 print( 0.1 + 0.1 + 0.1 - 0.3) ) # 5.551115123125783e-17
e-17 staat voor 10-17. Beide optellingen zouden gelijk moeten zijn aan 0, maar door afrondingsverschillen is het antwoord bij het optellen van drijvende-komma-getallen bijna nul.
print( 1e-0 ) # 1.0 print( 1e-1 ) # 0.1 print( 1e-2 ) # 0.01 print( 1e-3 ) # 0.001
Het optellen en delen gaat bij drijvende-komma-getallen dus
op een andere manier dan bij gehele getallen.
Python kent ook decimale getallen.
import decimal print( decimal.Decimal(0.1) + decimal.Decimal(0.1) + decimal.Decimal(0.1) - decimal.Decimal(0.3) ) # Decimal(0.0)
print( type( decimal.Decimal(5.43) ) ) # <class 'decimal.Decimal'>
Bij decimale getallen kun je de precisie instellen.
import decimal print( decimal.Decimal(1) / decimal.Decimal(7) ) # 0.1428571428571428571428571429 decimal.getcontext().prec = 4 print( decimal.Decimal(1) / decimal.Decimal(7) ) # 0.1429
print( type( decimal.Decimal(5.43) ) ) # <class 'decimal.Decimal'>
print( 2j + 3j ) # 5j print( 2.0j + 3.0j ) # 5j print( 2.1j + 3.2j ) # 5.300000000000001j
In de wiskunde wordt het complexe getal (0, 1) meestal aangegeven met de letter i, maar in de elektrotechniek, waar i gebruikt wordt voor stroomsterkte, wordt meestal de letter j gebruikt. Python volgt de notatie die gebruikelijk is in de elektrotechniek.
print( type( 5j ) ) # <class 'complex'> print( type( 5.3j ) ) # <class 'complex'>
from fractions import Fraction x = Fraction(3, 5) y = Fraction(2, 5) print(x) # 3/5 print(y) # 2/5 print(x + y) # 1 print(x - y) # 1/5
print( type(x) ) # <class 'fractions.Fraction'> print( type(x + y) ) # <class 'fractions.Fraction'>
print( Fraction('0.25') ) # Fraction(1, 4) print( Fraction('0.25') + Fraction('1.25') ) # Fraction(3, 2)
Voor breuken bestaan verschillende conversie-functies.
from fractions import Fraction print( (3.5).as_integer_ratio() ) # 7, 2 print( Fraction( *(3.5).as_integer_ratio() ) ) # 7/2
print( Fraction.from_float(1.75) ) # Fraction(7, 4)
We hebben hierboven gezien dat je van elke variabele de class kunt opvragen met behulp van het statement type(). Naast de classes die standaard in Python aanwezig zijn, kun je ook zelf classes maken. Dat gaat als volgt:
class MyClass: pass obj = MyClass() print(MyClass) #print(obj) # <__main__.MyClass object at 0x0000024D8ED0A900> print(type(MyClass)) # print(type(obj)) #
Met het commando 'class MyClass' maak je een nieuwe class.
Met 'pass' geef je aan dat het een class betreft,
waar je dus nog bijna niets mee kunt doen.
Het enige wat je ermee kunt doen is een object maken.
Met 'obj = MyClass()' maak je een object met de naam obj van class MyClass.
Vervolgens blijken MyClass en obj te bestaan, want je kunt ze printen.
Vervolgens wordt met type gekeken wat de classes zijn van MyClass en obj.
MyClass blijkt van de class 'type' te zijn.
Er zijn al wel een hoop methoden aanwezig.
De methoden bij een object kun je opvragen met het commando dir().
class MyClass: pass obj = MyClass() print('***** Methoden van MyClass *****') print(dir(MyClass)) print('***** Methoden van obj *****') print(dir(obj))
De output is:
***** Methoden van MyClass *****
['__class__', '__delattr__', '__dict__', '__dir__',
'__doc__', '__eq__', '__firstlineno__', '__format__',
'__ge__', '__getattribute__', '__getstate__', '__gt__',
'__hash__', '__init__', '__init_subclass__', '__le__',
'__lt__', '__module__', '__ne__', '__new__', '__reduce__',
'__reduce_ex__', '__repr__', '__setattr__', '__sizeof__',
'__static_attributes__', '__str__', '__subclasshook__',
'__weakref__'] ***** Methoden van obj ***** ['__class__', '__delattr__', '__dict__', '__dir__',
'__doc__', '__eq__', '__firstlineno__', '__format__',
'__ge__', '__getattribute__', '__getstate__', '__gt__',
'__hash__', '__init__', '__init_subclass__', '__le__',
'__lt__', '__module__', '__ne__', '__new__', '__reduce__',
'__reduce_ex__', '__repr__', '__setattr__', '__sizeof__',
'__static_attributes__', '__str__', '__subclasshook__',
'__weakref__']
Er miljoenen drijvende-komma-getallen denkbaar. Anders gezegd: Op basis van een class (bijvoorbeeld drijvende-komma-getallen) kun je een veelheid aan verschillende objecten (miljoenen drijvende-komma-getallen) creëren. Voorbeeld:
a = 2.3 b = 6.53 c = 21.43 pi = 3.14159 print( type(a),type(b)), type(c)), type(pi))
De getoonde variabelen a, b,c en pi nooemen we instance-variables van de class float. genoemd. Soms wordt dat naar het Nederlands vertaald vertaald met instantie-variabelen. Een instantie-variabele kan bij elk object weer een andere waarde hebben. Als je zelf een class maakt met het class-commando, kun je zo'n instance-varable direct aan een object koppelen. In onderstaand voorbeeld wordt de instance-variable straal gekoppeld aan de objecten cirkel_1 en cirkel_2::
class Cirkel: pass cirkel_1 = Cirkel() cirkel_1.straal = 4.774653 cirkel_2 = Cirkel() cirkel_2.straal = 3.183102 print( 2 * 3.14159 * cirkel_1.straal) # 30.000004236539997 print( 2 * 3.14159 * cirkel_2.straal) # 20.00000282436 print( type( Cirkel ) ) # <class 'type'> print( type( cirkel_1 ) ) # <class '__main__.Cirkel'> print( type( cirkel_2 ) ) # <class '__main__.Cirkel'>
Wat je vaker tegenkomt, is dat instance_variables via een method __init__() in de class-definitie aan het object wordt gekoppeld. Ook een berekening kan in de class-definitie worden opgenomen. Zo'n berekening wordt als functie aan de class gekoppeld. Een functie die aan een class is gekoppeld, wordt een method genoemd.
class Cirkel: def __init__(self, straal): self.straal = straal def omtrek(self): return 2 * 3.14159 * self.straal cirkel_1 = Cirkel(4.774653) cirkel_2 = Cirkel(3.183102) print( cirkel_1.omtrek() ) # 30.000004236539997 print( cirkel_2.omtrek() ) # 20.00000282436
self verwijst naar het object. Hoewel __init__() twee variabelen kent, namelijk self en straal, hoef je in de commando's cirkel_1 = Cirkel(4.774653) en cirkel_2 = Cirkel(3.183102) bij class Cirkel maar één parameter (straal) mee te geven.
Een class-variabele is gekoppeld aan een class. Een class-variable is ook benaderbaar als er nog geen enkel object van de class is aangemaakt. Een class-variabele kan benaderd worden vauit alle objecten die op basis van die class zijn gemaakt.
import math class Cirkel: pi = math.pi def __init__(self, straal): self.straal = straal def omtrek(self): return 2 * Cirkel.pi * self.straal print( Cirkel.pi ) # 3.141592653589793 cirkel_1 = Cirkel(4.774653) cirkel_2 = Cirkel(3.183102) print( cirkel_1.pi ) # 3.141592653589793 print( cirkel_1.straal ) # 4.774653 print( cirkel_1.omtrek() ) # 30.00002957648093 print( cirkel_2.pi ) # 3.141592653589793 print( cirkel_2.straal ) # 3.183102 print( cirkel_2.omtrek() ) # 20.000019717653956
Om alle objecten van een class te doorlopen, Maak je eerst een lege list als class_variabele aan. Als je een nieuw object aanmaakt, voeg je in het daarbij behorende __init__()-commando een verwijzing naar dat nieuwe object toe aan die list. De list bevat dan uiteindelijk verwijzingen naar alle eerder aangemaakte objecten. In het volgende voorbeeld zijn de objecten cirkels, die alle behoren tot de class Cirkel. In de volgende code gebruiken we self.__class__ voor de class waartoe de toe te voegen cirkel behoort.
import math class Cirkel: pi = math.pi alle_cirkels = [] def __init__(self, straal): self.straal = straal self.__class__.alle_cirkels.append(self) print( Cirkel.alle_cirkels ) cirkel_1 = Cirkel(1) print( Cirkel.alle_cirkels ) cirkel_2 = Cirkel(2) print( Cirkel.alle_cirkels ) cirkel_3 = Cirkel(3) print( Cirkel.alle_cirkels )
Je kunt bijvoorbeeld de som van de oppervlakten van de cirkels berekenen. In onderstaand voorbeeld wordt gebruik gemaakt van @staticmethod. Dat is een decorator die aangeeft dat de method die erop volgt, totaal_oppervlakte(), weliswaar is ondergebracht bij de class Cirkel, maar eigenlijk niets met die class te maken heeft. Een static method is aanwezig in een class omdat het op een of andere manier logisch is om de method daar onder te brengen. Er is wel een zekere functionaliteit die te maken heeft met de class, maar er hoeven geen objecten van de class aanwezig te zijn om de method uit te voeren. Een static method heeft geen parameter self of cls. Een static method is een method die gekoppeld is aan de class, maar niet aan een enkel object van de class. Vanuit een static method kunnen de variabelen die gedefinieerd zijn in de class, niet gewijzigd worden.
import math class Cirkel: pi = math.pi alle_cirkels = [] def __init__(self, straal): self.straal = straal self.__class__.alle_cirkels.append(self) def oppervlakte(self): return self.__class__.pi * self.straal * self.straal @staticmethod def totaal_oppervlakte(): totaal = 0 for c in Cirkel.alle_cirkels: totaal += c.oppervlakte() return totaal cirkel_1 = Cirkel(1) cirkel_2 = Cirkel(2) cirkel_3 = Cirkel(3) print( Cirkel.totaal_oppervlakte() ) # 43.982297150257104
Je roept een static method normaliter uit vanuit de class, niet vanuit een object. In het voorbeeld hierboven: Cirkel.totaal_oppervlakte(), en niet: cirkel_1.totaal_oppervlakte().
Class-methods zijn methods die niet aan een object zijn gekoppeld, maar aan een class. Het verschil met static methods houdt in dat (1) class-methods wel class_variabelen kunnen wijzigen en (2) in een class-method als eerste parameter een verwijzing naar de class zelf is opgenomen. Deze parameter wordt doorgaans niet self genoemd, maar cls. Een class-method moet worden voorafgegaan door de decorator @classmethod.
import math class Cirkel: pi = math.pi alle_cirkels = [] def __init__(self, straal): self.straal = straal self.__class__.alle_cirkels.append(self) def oppervlakte(self): return self.__class__.pi * self.straal * self.straal @classmethod def totaal_oppervlakte(cls): totaal = 0 for c in Cirkel.alle_cirkels: totaal += c.oppervlakte() return totaal print( Cirkel.totaal_oppervlakte() ) cirkel_1 = Cirkel(1) cirkel_2 = Cirkel(2) cirkel_3 = Cirkel(3) print( Cirkel.totaal_oppervlakte() ) print('===') print( cirkel_1.oppervlakte() ) print( cirkel_1.totaal_oppervlakte() )
Om de oppervlakte van cirkel één te weten te komen, moet je method cirkel_1.oppervlakte() uitvoeren, en niet cirkel_1.totaal_oppervlakte(), omdat totaal_oppervlakte een class-method is en de oppervlakte van alle aanwezige cirkels berekent, niet alleen die van cikel_1.
Geometrische vormen, zoals een cirkel, een rechthoek
of een vierkant, hebben allemaal eeen oppervlakte.
Om die oppervlaktes te berekenen, bestaan er formules.
Oppervlakte kun je beschouwen als een eigenschap die
bij de vorm hoort, of als het resultaat van een
formule.
In onderstaand programma is de oppervlakte het
resultaat van een method:
class Rechthoek: def __init__(self, hoogte, breedte): self.hoogte = hoogte self.breedte = breedte def oppervlakte(self): return self.hoogte * self.breedte def main(): rechthoekje = Rechthoek(2, 4) print( rechthoekje.oppervlakte() ) # 8 if __name__ == '__main__': main()
We gaan nu een aantal wijzigingen doorvoeren. (1) Allereerst vervangen we self.hoogte door self._hoogte en self.breedte door self._breedte. We plaatsen dus een underscore voor de variabele_namen. Hiermee geef je aan dat het niet is toegestaan dat deze variabelen van buitenaf worden gewijzigd. Met 'van buitenaf' wordt dan bedoeld 'van buiten de class-definitie'. (2) Vervolgens plaatsen we een regel met de tekst '@property' direct voorafgaand aan de regel 'def oppervlakte(self):'. (3) Als laatste vervangen we de aanroep 'rechthoekje.oppervlakte()' door 'rechthoekje.oppervlakte', d.w.z. dat we in de oproep de tekens () weglaten
class Rechthoek: def __init__(self, hoogte, breedte): self._hoogte = hoogte self._breedte = breedte @property def oppervlakte(self): return self._hoogte * self._breedte def main(): rechthoekje = Rechthoek(2, 4) print( rechthoekje.oppervlakte ) # 8 if __name__ == '__main__': main()
Met de decorator @property kun je dus wat door een method wordt berekend opvatten als een eigenschap van een object, dat niet zomaar gewijzigd mag worden. Wijziging mag alleen plaatsvinden vanuit een method van de class zelf.
class Rechthoek: def __init__(self, hoogte, breedte): self._hoogte = hoogte self._breedte = breedte @property def oppervlakte(self): return self._hoogte * self._breedte def main(): rechthoekje = Rechthoek(2, 4) print( rechthoekje.oppervlakte ) # 8 rechthoekje.oppervlakte = 3 if __name__ == '__main__': main()
heeft als output
8
Traceback (most recent call last): File "xxx.py", line 17, inmain() File "xxx.py", line 14, in main rechthoekje.oppervlakte = 3 AttributeError: property 'oppervlakte' of 'Rechthoek' object has no setter
De conversie-formules voor graden Celsius en graden Fahrenheit zijn:
f = c * 9 / 5 + 32 c = (f - 32) * 5 / 9
We maken een class Temperatuur, waarin we de temperatuur opslaan in graden Celsius.
class Temperatuur: def __init__(self): self._celsius = 0 @property def celsius_naar_fahrenheit(self): return self._celsius * 9 / 5 + 32 def main(): temp = Temperatuur() print( temp._celsius , '\u00B0C') # 0 °C print( temp.celsius_naar_fahrenheit , '\u00B0F' ) # 32 °F if __name__ == '__main__': main()
Omdat we ook andere temperaturen dan 0 °C willen kunnen weergeven, voegen we een setter toe aan de class Temperatuur.
class Temperatuur: def __init__(self): self._celsius = 0 @property def celsius_naar_fahrenheit(self): return self._celsius * 9 / 5 + 32 @celsius_naar_fahrenheit.setter def celsius_naar_fahrenheit(self, nieuwe_celsius): return nieuwe_celsius * 9 / 5 + 32 def main(): temp = Temperatuur() print( temp._celsius , '\u00B0C') # 0 °C print( temp.celsius_naar_fahrenheit , '\u00B0F' ) # 32 °F temp._celsius = 100 print( temp._celsius , '\u00B0C') # 100 °C print( temp.celsius_naar_fahrenheit , '\u00B0F' ) # 212 °F if __name__ == '__main__': main()
Vanuit functie main(), die niet gedefineerd is binnen
de class Temperatuur, kunnen we toch de waarde van
_celsius in object temp wijzigen.
Als we de naam celsius_naar_fahrenheit wijzigen in
_fahrenheit, dan lijkt setter
_fahrenheit op een variabele, die meeverandert met
_celsius.
class Temperatuur: def __init__(self): self._celsius = 0 @property def _fahrenheit(self): return self._celsius * 9 / 5 + 32 @_fahrenheit.setter def _fahrenheit(self, nieuwe_celsius): return nieuwe_celsius * 9 / 5 + 32 def main(): temp = Temperatuur() print( temp._celsius , '\u00B0C') # 0 °C print( temp._fahrenheit , '\u00B0F' ) # 32 °F temp._celsius = 100 print( temp._celsius , '\u00B0C') # 100 °C print( temp._fahrenheit , '\u00B0F' ) # 212 °F if __name__ == '__main__': main()
Overigens, de regel 'return nieuwe_celsius * 9 / 5 + 32' mag je vervangen door 'self._fahrenheit = nieuwe_celsius * 9 / 5 + 32'.
Het 'Liskov substitutie principe' betekent dat, als je super-class
hebt waarvan een sub-class worrdt afgeleid,
de sub-class dezelfde interface en implementatie moet erven van de super-class
en dat objecten van de sub-class de objecten van de super-class kunnen
vervangen.
Anders gezegd:
Het 'Liskov substitutie principe' zegt dat een afgeleid object
- laten we dat Derived noemen -
die erft van class - welke we Base noemen -
het Base-object moet kunnen vervangen
zonder de gewenste eigenschappen van een programma te veranderen.
Een voorbeeld: Veronderstel dat je denkt dat een vierkant een
variant is van een rechthoek, en dat je de class Vierkant afleidt van
de class Rechthoek.
De eigenschap waar we dan onze aandacht op richten is oppervlakte,
die is ondergebracht in de Base-class Rechthoek.
In het programma hieronder ontstaat een fout, als je
gebruik gaat maken van de functie resize().
De functie resize() kan gebruikt worden voor een object van
de class Rechthoek(), maar niet voor een object van de class
Vierkant.
class Rechthoek: def __init__(self, hoogte, breedte): self._hoogte = hoogte self._breedte = breedte @property def oppervlakte(self): return self._hoogte * self._breedte def resize(self, nieuwe_hoogte, nieuwe_breedte): self._hoogte = nieuwe_hoogte self._breedte = nieuwe_breedte class Vierkant(Rechthoek): def __init__(self, zijde): super().__init__(zijde, zijde) def main(): rechthoekje = Rechthoek(2, 4) print( rechthoekje.oppervlakte ) # 8 vierkantje = Vierkant(2) print( vierkantje.oppervlakte ) # 4 rechthoekje.resize(3, 5) print( rechthoekje.oppervlakte ) # 15 vierkantje.resize(3, 5) print( vierkantje.oppervlakte ) # 15 ???? if __name__ == '__main__': main()
De oorzaak van het feit dat je resize() niet kunt gebruiken voor een object van class Vierkant, is dat Rechthoek twee parameters vereist (hoogte en breedte) en Vierkant maar één (zijde). Vierkant heeft dus een andere interface als Rechthoek. Hoewel je een vierkant als een rechthoek kunt opvatten, kun je de class Vierkant beter niet programmeren als een subclass van de class Rechthoek, omdat dat heel gemakkelijk tot fouten kan leiden.
Om iets zinnigs te kunnen doen met een class, kun je verschillende dingen doen. Eén daarvan is dat je een class kunt baseren op een andere class. Hieronder volgt een voorbeeld, waarin een eenheid aan de de class float wordt toegevoegd.
class FloatUnit(float): def __new__(cls, value, unit): instance = super().__new__(cls, value) instance.unit = unit instance.value = value return instance def __repr__(cls): return str(cls.value) + ' ' + str(cls.unit) afstand = FloatUnit(1000, 'km') print(afstand) # 1000 km print(type(afstand)) # <class '__main__.FloatUnit'>
De output van dit programma is:
1000 km <class '__main__.FloatUnit'>
Je definieert eerst een methode __new__() die bij de class FloatUnit hoort.
Deze wordt uitgevoerd onmiddellijk na het maken van de lege class.
De methode super().__new__() hoort bij de class float.
Deze methode zorgt ervoor, dat de class FloatUnit een attribuut met de naam instance krijgt,
die een blauwdruk is voor een een float-waarde.
In de daarop volgende commando's worden twee attributen aan deze blauwdruk toegevoegd,
namelijk een waarde (value) en een eenheid (unit).
In het return-commando wordt de attribuut instance gekoppeld aan de class FloatUnit.
In dit voorbeeld volgt het print-commando wat je in de methode __repr__() hebt gedefinieerd.
In het volgende voorbeeld wordt naast __repr__() ook de methode __str__() gedefinieerd.
Als __str__() gedefinieerd is, volgt print() de methode __str__() en niet de methode __repr__().
import decimal class AmountOfMoney(decimal.Decimal): def __new__(cls, value, unit, sign): instance = super().__new__(cls, value) instance.unit = unit instance.value = value instance.sign = sign return instance def __str__(cls): return str(cls.sign) + ' ' + str(cls.value) def __repr__(cls): return str( cls.value) + ' ' + str(cls.unit) geldbedrag = AmountOfMoney(1000, 'euro', '\N{EURO SIGN}') print(type(geldbedrag)) # <class '__main__.AmountOfMoney'> print(geldbedrag) # € 1000 print(geldbedrag.__str__()) # € 1000 print(geldbedrag.__repr__()) # 1000 euro
Standaard JSON kent (1) geen commentaar, (2) geen komma's waarna niets volgt en (3) geen enkele aanhalingstekens bij strings.
Het omzetten van gegevens naar het JSON-formaat wordt serialization genoemd.
Het tegenovergestelde proces, deserialization, houdt in dat gegevens in JSON-formaat wordt omgezet in een in Python-gegevenstype.
De volgende Python-code zet de python-gegevenstypen om in strings met json-code.
import json def python_to_json(): print(json.dumps({"naam": "Jan", "leeftijd": 31})) # dictionary -> json-object print(json.dumps(["appel", "banaan"])) # list -> json-array print(json.dumps(("appel", "banaan"))) # tuple -> json-array print(json.dumps("hallo")) # string -> json-string print(json.dumps(56)) # integer -> json-number print(json.dumps(41.87)) # float -> json-number print(json.dumps(True)) # True -> json-true print(json.dumps(False)) # False -> json-false print(json.dumps(None)) # None -> json-null python_to_json()
Het programma heeft als output:
{"naam": "Jan", "leeftijd": 31} ["appel", "banaan"] ["appel", "banaan"] "hallo" 56 41.87 true false null
De volgende Python-code zet json-gegevens om naar python-gegevenstypen .
import json def json_to_python(): print(json.loads('{"naam": "Jan", "leeftijd": 31}')) # json-object -> dictionary print(json.loads('["appel", "banaan"]')) # json-array -> list print(json.loads('"hallo"')) # json-string -> string print(json.loads('56')) # json-number -> integer print(json.loads('41.87')) # json-number -> float print(json.loads('true')) # json-true -> True print(json.loads('false')) # json-false -> False print(json.loads('null')) # json-null -> None json_to_python()
Het programma heeft als output:
{'naam': 'Jan', 'leeftijd': 31} ['appel', 'banaan'] hallo 56 41.87 True False None
Als input-file nemen we infile.json met de volgende inhoud:
[ {"naam": "Jan", "leeftijd": 31}, ["appel", "banaan"], "hallo", 56, 41.87, true, false, null ]
Het Python-programma om het json-bestand in te lezen, ziet er als volgt uit:
import json def read_json_file(): with open("infile.json", mode="r", encoding="utf-8") as jsonfile: infile_data = json.load(jsonfile) print(infile_data) read_json_file()
Als output krijgen we:
[{'naam': 'Jan', 'leeftijd': 31}, ['appel', 'banaan'], 'hallo', 56, 41.87, True, False, None]
import json def write_json_file(): python_list = [{'naam': 'Jan', 'leeftijd': 31}, ['appel', 'banaan'], 'hallo', 56, 41.87, True, False, None] with open("outfile.json", mode="w", encoding="utf-8") as jsonfile: json.dump(python_list, jsonfile) write_json_file()
De inhoud van het weggeschreven bestand is:
[{"naam": "Jan", "leeftijd": 31}, ["appel", "banaan"], "hallo", 56, 41.87, true, false, null]
import json def write_json_file(): python_dict = { 'naam': 'Jan', 'leeftijd': 31, 'woonplaats': 'Rotterdam' } with open("outfile.json", mode="w", encoding="utf-8") as jsonfile: json.dump(python_dict, jsonfile) write_json_file()
De inhoud van het weggeschreven bestand is:
{"naam": "Jan", "leeftijd": 31, "woonplaats": "Rotterdam"}
In dit voorbeeld werd een string als key gebruikt. Er zijn een drietal gegevenstypen die in Json niet als key mogen worden gebruikt. Dat zijn dict, list en tuple. Overzicht:
Python data type | Toegestaan als JSON key | |
dict | ✗ | |
list | ✗ | |
tuple | ✗ | |
str | ✓ | |
int | ✓ | |
float | ✓ | |
bool | ✓ | |
None | ✓ |
Als input-file nemen we rechthoeken.json met de volgende inhoud:
[ { "illustration_upper_left_x": 0, "illustration_upper_left_y": 0, "element_x": 10, "element_y": 5, "element_width": 300, "element_height": 30, "element_top": 10, "element_right": 10, "element_bottom": 10, "element_color": "lightGreen", "text_x": 15, "text_y": 24, "text_length": 100, "text": "the quick brown fox jumps over the lazy dog" }, { "illustration_upper_left_x": 0, "illustration_upper_left_y": 0, "element_x": 10, "element_y": 5, "element_width": 300, "element_height": 30, "element_top": 10, "element_right": 10, "element_bottom": 10, "element_color": "lightGreen", "text_x": 15, "text_y": 24, "text_length": 100, "text": "filmquiz bracht knappe ex-yogi van de wijs" }, { "illustration_upper_left_x": 0, "illustration_upper_left_y": 0, "element_x": 10, "element_y": 5, "element_width": 300, "element_height": 30, "element_top": 10, "element_right": 10, "element_bottom": 10, "element_color": "lightGreen", "text_x": 15, "text_y": 24, "text_length": 100, "text": { "text_1": "Als beginnend concertist debuteerde een ", "text_2": "fijngevoelige gitarist, hierna improviseerden", "text_3": "jeugdige klankkunstenaars levendig maar", "text_4": "notenblind op Peruviaanse quena's, robuuste", "text_5": "slagwerkers trommelden uitzinnige volksmuziek,", "text_6": "waarna xylofonisten 'Yesterday' zongen.; " } } ]
Het Python-programma om het json-bestand rechthoeken.json in te lezen, ziet er als volgt uit:
import json def read_json_file(): with open("rechthoeken.json", mode="r", encoding="utf-8") as jsonfile: infile_data = json.load(jsonfile) print(infile_data) read_json_file()
De outputfile bestaat uit één regel waarin de gehele list is opgenomen.
Als je binnen je programma een beter beeld wilt hebben van wat je hebt ingelezen, kun je de ingelezen data formatteren. Bijvoorbeeld:
import json def read_json_file(): with open("rechthoeken.json", mode="r", encoding="utf-8") as jsonfile: infile_data = json.load(jsonfile) formatted_data = json.dumps(infile_data, indent=4, separators=(". ", " = ")) print(formatted_data) read_json_file()
De output wordt dan als volgt geformatteerd:
[ { "illustration_upper_left_x" = 0. "illustration_upper_left_y" = 0. "element_x" = 10. "element_y" = 5. "element_width" = 300. "element_height" = 30. "element_top" = 10. "element_right" = 10. "element_bottom" = 10. "element_color" = "lightGreen". "text_x" = 15. "text_y" = 24. "text_length" = 100. "text" = "the quick brown fox jumps over the lazy dog" }. { "illustration_upper_left_x" = 0. "illustration_upper_left_y" = 0. "element_x" = 10. "element_y" = 5. "element_width" = 300. "element_height" = 30. "element_top" = 10. "element_right" = 10. "element_bottom" = 10. "element_color" = "lightGreen". "text_x" = 15. "text_y" = 24. "text_length" = 100. "text" = "filmquiz bracht knappe ex-yogi van de wijs" }. { "illustration_upper_left_x" = 0. "illustration_upper_left_y" = 0. "element_x" = 10. "element_y" = 5. "element_width" = 300. "element_height" = 30. "element_top" = 10. "element_right" = 10. "element_bottom" = 10. "element_color" = "lightGreen". "text_x" = 15. "text_y" = 24. "text_length" = 100. "text" = { "text_1" = "Als beginnend concertist debuteerde een ". "text_2" = "fijngevoelige gitarist, hierna improviseerden". "text_3" = "jeugdige klankkunstenaars levendig maar". "text_4" = "notenblind op Peruviaanse quena's, robuuste". "text_5" = "slagwerkers trommelden uitzinnige volksmuziek,". "text_6" = "waarna xylofonisten 'Yesterday' zongen.; " } } ]
In Python creëren we een dictionary, die we op twee manieren omzetten naar json-formaat. De tweede manier bevat minder spaties, en is daardoor korter.
import json def create_json(): # creëer dictionary json_dict json_dict = { 1: { "illustration_upper_left_x": 0, "illustration_upper_left_y": 0, "element_x": 10, "element_y": 5, "element_width": 300, "element_height": 30, "element_top": 10, "element_right": 10, "element_bottom": 10, "element_color": "lightGreen", "text_x": 15, "text_y": 24, "text_length": 100, "text": "the quick brown fox jumps over the lazy dog" }, 2: { "illustration_upper_left_x": 0, "illustration_upper_left_y": 0, "element_x": 10, "element_y": 5, "element_width": 300, "element_height": 30, "element_top": 10, "element_right": 10, "element_bottom": 10, "element_color": "lightGreen", "text_x": 15, "text_y": 24, "text_length": 100, "text": "filmquiz bracht knappe ex-yogi van de wijs" } } # zet dictionary json_dict om in string json_data json_data = json.dumps(json_dict) print('aantal bytes in json_data :', len(json_data)) # schrijf json_data weg als bestand outfile1.json with open("outfile1.json", mode="w", encoding="utf-8") as output_file: output_file.write(json_data) # zet dictionary json_dict om in string mini_json mini_json = json.dumps(json_dict, indent=None, separators=(",", ":")) print('aantal bytes in mini_json :', len(mini_json)) # schrijf mini_jason weg als bestand outfile2.json with open("outfile2.json", mode="w", encoding="utf-8") as output_file: output_file.write(mini_json) create_json()
De output is:.
De volgende python-code zet het csv-bestand 'infile.csv' om naar het json-bestand 'outfile.json':
import csv import json def csv_to_json(csvFilePath, jsonFilePath): jsonArray = [] try: with open(csvFilePath, encoding='utf-8') as csvf: csvReader = csv.DictReader(csvf) for row in csvReader: jsonArray.append(row) with open(jsonFilePath, 'w', encoding='utf-8') as jsonf: jsonString = json.dumps(jsonArray, indent=4) jsonf.write(jsonString) except FileNotFoundError: print("csv_to_json: csv-input-file niet gevonden " + csvFilePath) def main(): csvFilePath = r'infile.csv' jsonFilePath = r'outfile.json' csv_to_json(csvFilePath, jsonFilePath) if __name__ == '__main__': main()
Een gewone manier om een variabele te definiëren is :
a = 15 print(a)
De output is:
Je kunt specificeren welk type de variabele moet hebben :
a: int = 15 print(a)
De output blijft:
Intern in Python wordt in de directory __annotations__ de type-hint opgeslagen:
a: int = 15 print(a) print(__annotations__)
De output wordt:
De python-interpreter doet helemaal niets met de toevoeging van ': int'. Het is meer bedoeld als herinnering voor de programmeur, in de trant van 'het is de bedoeling dat een variabele met de naam a van het type int is. ' Als een programmeur zich niet aan zo'n voornemen houdt, genereert de python-interpreter geen errors of warnings. Het volgende programma kent de waarde 15.72 van het type float toe aan variabele a, maar specificeert dat a een integer-waarde zou moeten hebben. Niettemin wordt er bij uitvoeriing van het programma geen waarschuwing gegenereerd:
a: int = 15.72 print(a) print(__annotations__)
De output is:
Toevoegingen als ': int' of ': float' worden 'type hints' genoemd Sommige editors (PyCharm) en programma's van externe partijen (MyPy) gebruiken type hints om inconsistenties in programma's op te sporen.
s: str = "abcd" i: int = 15 f: float = 41.25 l: list = [ "a", "b" ] t: tuple = ( "a", "b" ) d: dict = { 1 : "a" } b: bool = True n: None = None print(s) print(i) print(f) print(l) print(t) print(d) print(b) print(n) print('-----') print(__annotations__)
De output is:
In het volgende programma worden drie functies zonder type hints gedefinieerd:
def func_1(var): return print(var) def func_2(var = "x"): return print(var) def func_3(var = "y"): print(var) return var func_1("a") func_2() func_2("b") func_3() func_3("c")
Bij func_2 is de default-waarde voor variabele var gelijkgesteld aan "x". Bij func_3 is deze default-waarde gelijkgesteld aan "y". De output is:
Je kunt aan deze functies type-hints toevoegen De type-hint voor de return-waarde van de functie geeft je aan met '->':
def func_1(var: str): return print(var) def func_2(var: str = "x"): return print(var) def func_3(var: str = "y") -> str: print(var) return var func_1("a") func_2() func_2("b") func_3() func_3("c") print('-----') print(__annotations__) print(func_1.__annotations__) print(func_2.__annotations__) print(func_3.__annotations__)
De output wordt
De volgende code definieert een list, een tuple en een dictionary:
namen = ["Jan", "Piet", "Kees"] versies = (3, 7, 2) opties = {"gecentreerd": False, "met_hoofdletters": True} print(namen[0], namen[1], namen[2]) print(versies[0], versies[1], versies[2]) print(opties["gecentreerd"], opties["met_hoofdletters"])
De output is:
Je kunt aan deze code type hints voor de individuele objecten in de list toevoegen. Je moet daarvoor wel delen van de standaard-module typing importeren.
from typing import Dict, List, Tuple namen: List[str] = ["Jan", "Piet", "Kees"] versies: Tuple[int, int, int] = (3, 7, 1) opties: Dict[str, bool] = {"gecentreerd": False, "met_hoofdletters": True} print(namen[0], namen[1], namen[2]) print(versies[0], versies[1], versies[2]) print(opties["gecentreerd"], opties["met_hoofdletters"]) print('-----') print(__annotations__)
De output wordt:
We gaan uit van een programma waarin een class en een object worden gedefinieerd.
import math class Mal(): pi = math.pi def __init__(self, radius): self.radius = radius def omtrek_en_oppervlakte(self): self.omtrek = 2 * self.pi * self.radius self.oppervlakte = self.pi * self.radius * self.radius return self.omtrek, self.oppervlakte def main(): object_1 = Mal(10) (omtrek, oppervlakte) = object_1.omtrek_en_oppervlakte() print(omtrek, oppervlakte) main()
De output wordt:
Aan het programma voegen we type-hints toe.
import math class Circle_template(): print('----- Circle_template() ----- class definiton -----') pi: float = math.pi print('Circle_template: __annotations__ =', __annotations__) def __init__(self, radius): print('-----__init__(self, radius) ----- method') self.radius: float = radius print('__init__(self, radius): self.__annotations__ ==', self.__annotations__) def omtrek_en_oppervlakte(self): print('----- omtrek_en_oppervlakte(self) ----- method') self.omtrek: float = 2 * self.pi * self.radius self.oppervlakte: float = self.pi * self.radius * self.radius print('omtrek_en_oppervlakte(self): __annotations__ =', __annotations__) print('omtrek_en_oppervlakte(self): self.__annotations__ ==', self.__annotations__) return self.omtrek, self.oppervlakte def main(): print('----- main() ----- function') circle = Circle_template(10) print( 'main(): circle.__annotations__ ==', circle.__annotations__) (omtrek, oppervlakte) = circle.omtrek_en_oppervlakte() print(omtrek, oppervlakte) print('----- program -----') main()
De output wordt:
Wat opvalt is dat omtrek en oppervlakte niet worden opgenomen in de annotations. Dit schijnt te maken te hebben met het feit dat een class een soort mal is, een template, dat als het gedefinieerd wordt, nog geen object is. Dit is voor mij vooralsnog onbekend terrein en ik ga er verder niet op in.
Refactoring gaat over het herschrijven van bestaande programmatuur. Het gaat erom om onduidelijke of onnodig gecompliceerde code om te zetten in goede code, zó, dat het programma precies blijft doen wat het altijd al deed. Waarom zou je bestaande programma's willen herschrijven? Daarvoor zijn verschillende redenen te bedenken:
In het volgende programma voldoet de functie containsEven aan de regel, dat je maar vijf statements in een functie mag hebben, maar de functie minimum niet. De functie containsEven(arr) checkt of de twee-dimensionale array arr een even getal bevat. De functie minimum(arr) bepaalt het laagste getal in de twee-dimensionale array arr.
def containsEven(arr): for x in arr: for y in x: if y % 2 == 0: return True return False def minimum(arr): r = (9**9)**9 for x in arr: for y in x: if y < r: r = y return r def main(): arr = [ [21, 23, 67], [35, 67, 33], [41, 58, 89], [23, 11, 73], ] r = containsEven(arr) print(r) r = minimum(arr) print(r) main()
De output wordt:
Als je er niet op let worden functies of methoden al maar langer. Daarmee wordt het lastiger ze te begrijpen.
Een hulpmiddel om te komen tot "five lines" is het refactoring-patroon "extract method. Deze passen we toe op bovenstaande functie minimum().
def minimum(arr): r = (9**9)**9 for x in arr: for y in x: #-------- if y < r: r = y #-------- return r def main(): arr = [ [21, 23, 67], [35, 67, 33], [41, 58, 89], [23, 11, 73], ] r = minimum(arr) print(r) main()
def minimum(arr): r = (9**9)**9 for x in arr: for y in x: #-------- #-------- return r #-------- def min(): if y < r: r = y #-------- def main(): arr = [ [21, 23, 67], [35, 67, 33], [41, 58, 89], [23, 11, 73], ] r = minimum(arr) print(r) main()
def minimum(arr): r = (9**9)**9 for x in arr: for y in x: #-------- r = min(r, arr, x, y) #-------- return r #-------- def min(r, arr, x, y): if y < r: r = y return r #-------- def main(): arr = [ [21, 23, 67], [35, 67, 33], [41, 58, 89], [23, 11, 73], ] r = minimum(arr) print(r) main()
def minimum(arr): r = (9**9)**9 for x in arr: for y in x: r = min(r, arr, x, y) return r def min(r, arr, x, y): if y < r: r = y return r def main(): arr = [ [21, 23, 67], [35, 67, 33], [41, 58, 89], [23, 11, 73], ] r = minimum(arr) print(r) main()
Een korte vertaling van de regel "either call or pass' zou kunnen zijn: "ofwel aanroepen, ofwel doorgeven, maar niet beide". Een functie zou dus ofwel methoden behorende bij een object moeten aanroepen, ofwel het object doorgeven als een argument, maar niet beide. Als je het voorbeeld dat daarbij in het boek "Five lines of code" wordt gegeven, vertaalt naar Python, krijg je zoiets als
def average(arr): return sum(arr) / arr.__len__() def main(): arr = [21, 23, 67, 35, 67, 33, 41, 58, 89, 23, 11, 84] print(average(arr)) main()
In sum(arr) wordt het object arr als argument doorgegeven aan de functie sum(), maar in arr.__len__() wordt een method van het object aangeroepen. In Python is dat nogal een gekunsteld voorbeeld, want arr.__len__() is een heel ongewone schrijfwijze van len(arr). Volgens de regel "either pass or call" zou je dit programma moeten verbeteren tot
def average(arr): return sum(arr) / len(arr) def main(): arr = [21, 23, 67, 35, 67, 33, 41, 58, 89, 23, 11, 84] print(average(arr)) main()
De reden van de regel "either call or pass" wordt uitgelegd als: Elk statement in een functie zou hetzelfde abstractie-niveau moeten hebben. Het doorgeven van een object als argument leidt in de regel tot een hoger abstractie-niveau dan het aanroepen van een method van een object. Vandaar dat je binnen een functie moet kiezen voor het één of het ander.
De verschillende vertakkingen binnen een if-statement moet je zo veel mogelijk door afzonderlijke functie/methoden laten afhandelen. In de volgende functie zie je drie if-statements staan.
import math as m def report_primes(n): for i in range(2, n): if is_prime(i): print(i) def is_prime(i): s = m.floor(m.sqrt(i)) + 1 has_factor = False for f in range(2, s): if i % f == 0: has_factor = True if has_factor == False: return True report_primes(100)
Als je het eerste if_statement vervangt door een aparte functie, krijg je
import math as m def report_primes(n): for i in range(2, n): report_prime(i) def report_prime(i): if is_prime(i): print(i) def is_prime(i): s = m.floor(m.sqrt(i)) + 1 has_no_factor = True for f in range(2, s): if i % f == 0: has_no_factor = False return has_no_factor report_primes(100)
Als ik de andere if-statements in een aparte functie probeer onder te brengen, vind ik het programma er niet duidelijker op worden.
De regel luidt: Gebruik geen else in een if-statement, behalve wanneer we te maken hebben met een variabele waarvan we de inhoud niet kunnen bepalen (bijvoorbeeld het indrukken van toets). De reden om else zo min mogelijk te gebruiken is omdat het verwarrend kan zijn onder welke omstandigheden tot de else-vertakking wordt overgegaan. In onderstaand programma
def gemiddelde(getallen_reeks): if len(getallen_reeks) == 0: print('een lege getallenreeks is niet toegestaan') else: return sum(getallen_reeks) / len(getallen_reeks) def main(): print(gemiddelde([1, 2, 3])) print(gemiddelde([100, 200, 300])) print(gemiddelde([])) if __name__ == '__main__': main()
vervangen we de vertakkingen van de if- en else-clausules door opeenvolgende functie-aanroepen. Elke van die functies begint met een if-statement zonder else-clausule.
def gemiddelde(getallen_reeks): check_getallen_reeks_leeg(getallen_reeks) return bereken_gemiddelde(getallen_reeks) def check_getallen_reeks_leeg(getallen_reeks): if len(getallen_reeks) == 0: print('een lege getallenreeks is niet toegestaan') return None def bereken_gemiddelde(getallen_reeks): if len(getallen_reeks) != 0: return sum(getallen_reeks) / len(getallen_reeks) def main(): print(gemiddelde([1, 2, 3])) print(gemiddelde([100, 200, 300])) print(gemiddelde([])) if __name__ == '__main__': main()
De output van beide programma's is hetzelfde:
2.0 200.0 een lege getallenreeks is niet toegestaan None
Als je deze werkwijze hanteert, wordt de programmatuur er dan duidelijker op? Als je niet in de gaten hebt, dat de functies check_getallen_reeks_leeg() en bereken_gemiddelde() beide met een if-statement beginnen, en de voorwaarden die bij die if-statements horen samen alle mogelijkheden vertegenwoordigen ( x == 0 en x != 0 vertegenwoordigen samen alle mogelijke waarden die x kan aannemen ), dan lijkt de inhoud van de functie gemiddelde() op een sequentie van een aantal acties.
Het boek 'Five lines of code' nodigt uit om onderscheid te maken tussen checks en decisions. Een if_statement is een check als er enkel gekeken wordt of er een bepaald iets aan de hand is. Een if-elif-statement kun soms meer opvatten als decision, als een beslissing die genomen moet worden. De filosofie is dat je beslissingen zo lang mogelijk moet uitstellen, dus zo laat mogelijk moet nemen. Om elif-clausules te vermijden propageert het boek 'Five lines of code' het refactoring-pattern 'Replace code with classes'.
Als je in een if-elif-constructie te maken hebt met verschillende categorieën, bijvoorbeeld met rood, oranje en groen (bij stoplichten) of small, medium en large (bij kledingmaten), breng dan elk van die categorieën onder in een eigen class. Als voorbeeld nemen we het volgende programma:
def comment_bank(bank): if bank.upper() == 'ASN': print('ASN') print('duurzaam en geen wapenindustrie') elif bank.upper() == 'TRIODOS': print('TRIODOS') print('duurzaam') elif bank.upper() == 'ING': print('ING') print('voert oranje leeuw als mascotte') elif bank.upper() == 'RABO': print('RABO') print('was ooit coöperatief') elif bank.upper() == 'ABN': print('ABN') print('werd ooit opgekocht door de overheid') def main(): bank = input('Bank ') comment_bank(bank) if __name__ == '__main__': main()
De gebruiker wordt gevraagd de naam van een bank in te voeren. Vervolgens wordt wat informatie over de bank getoond. De functie comment_bank() bevat een uitgebreid if-elif-else-statement. Dit programma gaan we in een aantal stappen herschrijven. Als een gebruiker ASN, Asn, asn of iets dergelijks intikt, verschijnt de tekst
ASN duurzaam en geen wapenindustrie
class Asn_bank(): name = 'ASN' def process_bank(self): print('ASN') print('duurzaam en geen wapenindustrie')
Iets soortgelijks doen we voor de andere banken die een gebruiker kan kiezen. We zorgen ervoor dat in de nieuwe classes het attribuut en de methode dezelfde naam hebben. We krijgen dan:
class Asn_bank(): name = 'ASN' def process_bank(self): print('ASN') print('duurzaam en geen wapenindustrie') class Triodos_bank(): name = 'TRIODOS' def process_bank(self): print('TRIODOS') print('duurzaam') class Ing_bank(): name = 'ING' def process_bank(self): print('ING') print('voert oranje leeuw als mascotte') class Rabo_bank(): name = 'RABO' def process_bank(self): print('RABO') print('was ooit coöperatief') class Abn_bank(): name = 'ABN' def process_bank(self): print('ABN') print('werd ooit opgekocht door de overheid')
Elke class heeft een attribuut name en een methode process_bank(). Het programma begint met het maken van een object voor elke class.
asn = Asn_bank() triodos = Triodos_bank() ing = Ing_bank() rabo = Rabo_bank() abn = Abn_bank()
We nemen deze objecten op in een list.
bank_list = [asn, triodos, ing, rabo, abn]
Dit doen we, omdat we daarmee de verschillende classes één voor één kunnen benaderen. We vragen aan een gebruiker een banknaam in te tikken.
bank_input = input('Bank ')
Daarna doorlopen we de lijst bank-objecten. Als we bij de bank zijn aangekomen, die de gebruiker heeft gespecificeerd, voeren we de methode process_bank() uit.
Het programma ziet er in zijn geheel dan als volgt uit:
class Asn_bank(): name = 'ASN' def process_bank(self): print('ASN') print('duurzaam en geen wapenindustrie') class Triodos_bank(): name = 'TRIODOS' def process_bank(self): print('TRIODOS') print('duurzaam') class Ing_bank(): name = 'ING' def process_bank(self): print('ING') print('voert oranje leeuw als mascotte') class Rabo_bank(): name = 'RABO' def process_bank(self): print('RABO') print('was ooit coöperatief') class Abn_bank(): name = 'ABN' def process_bank(self): print('ABN') print('werd ooit opgekocht door de overheid') def main(): # Maak bank-instellingen asn = Asn_bank() triodos = Triodos_bank() ing = Ing_bank() rabo = Rabo_bank() abn = Abn_bank() # Maak een lijst met banken bank_list = [asn, triodos, ing, rabo, abn] # Tik een bank in bank_input = input('Bank ') for bank in bank_list: if bank_input.strip().upper() == bank.name.upper(): bank.process_bank() if __name__ == '__main__': main()
We hebben nu het programma zodanig herschreven dat
het if-elif-statement vervangen is door een if-statement
zonder elif-clausules.
Als we gebruik maken van een dictionary in plaats van een list,
kan het programma zonder if-statement worden geschreven.
class Asn_bank(): def process_bank(self): print('ASN') print('duurzaam en geen wapenindustrie') class Triodos_bank(): def process_bank(self): print('TRIODOS') print('duurzaam') class Ing_bank(): def process_bank(self): print('ING') print('voert oranje leeuw als mascotte') class Rabo_bank(): def process_bank(self): print('RABO') print('was ooit coöperatief') class Abn_bank(): def process_bank(self): print('ABN') print('werd ooit opgekocht door de overheid') def main(): # Maak bank-instellingen asn = Asn_bank() triodos = Triodos_bank() ing = Ing_bank() rabo = Rabo_bank() abn = Abn_bank() # Maak dictionary met bank-instellingen bank_dict = {'ASN': asn, 'TRIODOS': triodos, 'ING': ing, 'RABO': rabo, 'ABN': abn } bank_input = input('bank ') try: bank_dict[bank_input.upper()].process_bank() except: pass if __name__ == '__main__': main()
Als voorbeeld nemen we het volgende programma:
def handle_event(invoer): print('handle_input(' + invoer + ')') if invoer == 'a': print('naar links') move_horizontal(-1) elif invoer == 'd': print('naar rechts') move_horizontal(1) elif invoer == 'w': print('omhoog') move_vertical(-1) elif invoer == 's': print('omlaag') move_vertical(1) def move_horizontal(x): print('move horizontal ' + str(x)) def move_vertical(y): print('move vertical ' + str(y)) def main(): invoer = input('a=links, d=rechts, w=omhoog, s=omlaag ') handle_event(invoer) if __name__ == '__main__': main()
Als een gebruiker een van de toetsen a, w, d of s indrukt, gaat er iets op het scherm naar links, naar boven , naar rechts of naar beneden. Het op en neer of omhoog en omlaag gaan wordt verzorgd door de functies move_vertical() en move_horizontal(). We willen in dit programma de functies move_horizontal() en move_vertical() dichterbij elkaar brengen. Daartoe creëren we voor elk van de vier mogelijkheden 'naar links', 'naar rechts, 'naar boven' en 'naar beneden' een class.
class Left(Invoer): def handle(): move_horizontal(-1) class Right(Invoer): def handle(): move_horizontal(1) class Up(Invoer): def handle(): move_vertical(-1) class Down(Invoer): def handle(): move_vertical(1)
Deze vier classes hebben dezelfde structuur. Merk op dat we bij bij de method handle() geen parameter self meegeven. Dat betekent dat het een class-method is. We hoeven geen gebruik te maken van objecten die zijn gebaseerd op Left, Right, Up en Down. De oorspronkelijke code herschrijven we nu.
class Left(): def handle(): move_horizontal(-1) class Right(): def handle(): move_horizontal(1) class Up(): def handle(): move_vertical(-1) class Down(): def handle(): move_vertical(1) def move_horizontal(x): print('move_horizontal(' + str(x) + ')') def move_vertical(y): print('move_vertical(' + str(y) + ')') def main(): prompt = 'a=links, d=rechts, w=omhoog, s=omlaag ' invoer = input(prompt) if invoer == 'a': invoer_class = Left elif invoer == 'd': invoer_class = Right elif invoer == 'w': invoer_class = Up elif invoer == 's': invoer_class = Down invoer_class.handle() if __name__ == '__main__': main()
Als een gebruiker een keuze heeft gemaakt, wordt afhankelijk wat hij heeft ingetikt, in de variabele invoer_class een verwijzing gemaakt naar één van de classes Left, Right, Up or Down. Omdat in elk van die classes de method handle() een andere inhoud heeft, wordt steeds de bijbehorende move-actie uitgevoerd.
In het volgende programma lijkt de functie rekening_bijwerken() overbodig.
class database: def update_rekening(rekening, bedrag): print('update_rekening :', bedrag, '->', rekening) def rekening_bijwerken(rekening, bedrag): database.update_rekening(rekening, bedrag) def overboeken(vanaf_rekening, naar_rekening, bedrag): rekening_bijwerken(vanaf_rekening, -bedrag) rekening_bijwerken(naar_rekening, bedrag) def main(): overboeken('ABN123', 'ING456', 543.21) if __name__ == '__main__': main()
Je kunt de code van de functie rekening_bijwerken() direct opnemen in de functie overboeken().
class database: def update_rekening(rekening, bedrag): print('update_rekening :', bedrag, '->', rekening) def overboeken(vanaf_rekening, naar_rekening, bedrag): database.update_rekening(vanaf_rekening, -bedrag) database.update_rekening(naar_rekening, bedrag) def main(): overboeken('ABN123', 'ING456', 543.21) if __name__ == '__main__': main()
Soms wordt de programmatuur begrijpelijker als een ingewikkelde functie wordt opgesplitst in functies die elk wat eenvoudiger te begrijpen zijn. In een spel kan een functie die antwoord geeft op de vraag 'welke vervolgacties zijn mogelijk?' soms beter gesplitst worden in bijvoorbeeld 'welke vervolgacties zijn mogelijk op de linkerflank?' en 'welke vervolgacties zijn mogelijk op de rechterflank?'.
Het woordenboek geeft als vertaling voor interface de woorden
grensvlak, raakvlak en koppeling.
In deze paragraaf gaat het over interfaces zoals die voorkomen
in de programmeertaal Java.
Interfaces in Java komen overeen met classes in Python,
die als blauwdruk fungeren voor andere classes.
Ze geven aan welke methods een class moet hebben,
maar niet wat die methods moeten doen.
Vertaald naar Python bevatten de methods van een interface
enkel het statement pass.
Volgens de regel 'Gebruik overerving alleen bij interfaces'
moet je alleen overerving gebruiken als de superclass
enkel de namen van de te definiëren methoden doorgeeft.
Wat die methoden doen, moet je in de class zelf opgeven.
Overerving wordt vaak gebruikt om een default implementatie
van een method te krijgen.
De nadelen daarvan zijn vaak veel ingrijpender dan de voordelen.
Code die door verschillende subklassen wordt gebruikt,
veroorzaakt koppelingen.
Vrij vertaald betekent dit: Gebruik overerving zo min mogelijk;
geef de vookeur aan composition.
Er zijn een aantal pakketten waarmee je functies en methodes die nergens worden aangeroepen in een Python-programma, kunt opsporen. Op internet vond ik (op 11-11-2024):
Ik kan mij goed voorstellen dat er situaties zijn, dat een programma begrijpelijker en beter onderhoudbaar is, wanneer wordt onderkend dat een aantal classes beter samengevoegd kunnen worden tot één class. Maar het is mij niet gelukt om in Python van een werkend programma te verzinnen, dat eenvoudig en kort genoeg is, en niet gekunsteld, om als voorbeeld te dienen.
Het volgende code-fragment bevat twee vertakkingen waarop de code 'x = 0'volgt.
if a > 0: x = 0 if a == 0: x = 1 if a < 0: x = 0
Deze code kun je herschrijven tot
if a == 0: x = 1 else x = 0
of
x = 0 if a == 0: x = 1
Welke codering je voorkeur heeft is een beetje een kwestie van smaak. De volgende oplossing die 'or' gebruikt vind ik in dit kleine voorbeeld te omslachtig.
if a < 0 or a > 0: x = 0 if a == 0: x = 1
Het is van belang de if-statements zo begrijpelijk mogelijk te houden. Soms is het prettig een voorwaarde in een aparte functie te evalueren.
def check_perioden_overlappen_elkaar( periode_1_vanaf, periode_1_tm, periode_2_vanaf, periode_2_tm): if periode_2_tm < periode_1_vanaf or periode_1_tm < periode_2_vanaf: return False else: return True def main(): print('Overlappen twee perioden elkaar?') periode_1_vanaf = input('Eerste periode, vanaf-datum (JJJJMMDD) ') periode_1_tm = input('Eerste periode, t/m-datum (JJJJMMDD) ' ) periode_2_vanaf = input('Tweede periode, vanaf-datum (JJJJMMDD) ') periode_2_tm = input('Tweede periode, t/m-datum (JJJJMMDD) ') print(' ') print('Eerste periode: ', periode_1_vanaf, 't/m', periode_1_tm) print('Tweede periode: ', periode_2_vanaf, 't/m', periode_2_tm) if check_perioden_overlappen_elkaar( periode_1_vanaf, periode_1_tm, periode_2_vanaf, periode_2_tm): print('De perioden overlappen elkaar') else: print('De perioden overlappen elkaar niet.') if __name__ == '__main__': main()
Met 'situaties met zij-effecten' bedoelen we condities die waarden toekennen aan variabelen, foutsituaties genereren, iets afdrukken, iets naar een file wegschrijven, e.d.